<?php

require_once 'Handler/Url.php';
require_once 'Handler/Resource.php';
require_once 'Handler/Cache.php';
require_once 'Handler/Data.php';
require_once 'Handler/Template.php';


/**
 * If Layered Content is being used stand-alone without a web framework, simply
 * create a new instance and call run(). If using in conjunction with a web
 * framework see the rest of the methods.
 */
class Layered_Site
{
	/**
	 * The system configuration settings.
	 *
	 * @var Zend_Config
	 */
	protected $config;
	
	/**
	 * URL Handler to handle how we create and use our URLs. This could be
	 * replaced by your own class with the same methods.
	 *
	 * @var LayeredContent_Handler_Url
	 */
	protected $urlHandler;
	
	/**
	 * URL Handler to handle how we create and use our URLs. This could be
	 * replaced by your own class with the same methods.
	 *
	 * @var LayeredContent_Handler_Resource
	 */
	protected $resourceHandler;
	
	/**
	 * URL Handler to handle how we create and use our URLs. This could be
	 * replaced by your own class with the same methods.
	 *
	 * @var LayeredContent_Handler_Cache
	 */
	protected $cacheHandler;
	
	/**
	 * URL Handler to handle how we create and use our URLs. This could be
	 * replaced by your own class with the same methods.
	 *
	 * @var LayeredContent_Handler_Data
	 */
	protected $dataHandler;
	
	protected $root;
	
	protected $appRoot;
	
	/**
	 * Constructor
	 */
	public function __construct()
	{
		$this->root = dirname(__FILE__);
		$this->appRoot = $this->root . '/application';
		$this->loadConfig();
		$this->urlHandler = new LayeredContent_Handler_Url();
		$this->resourceHandler = new LayeredContent_Handler_Resource();
		$this->cacheHandler = new LayeredContent_Handler_Cache();
		$this->dataHandler = new LayeredContent_Handler_Data();
	}
	
	public function init()
	{
		set_include_path('.' . PATH_SEPARATOR . $this->root . '/models' . PATH_SEPARATOR . get_include_path());
		
		$this->initHandler($this->urlHandler);
		$this->initHandler($this->resourceHandler);
		$this->initHandler($this->cacheHandler);
		$this->initHandler($this->dataHandler);
	}
	
	/**
	 * Override the built-in handlers for custom functionality.
	 *
	 * @param string $handlerName The handler's part or full name (e.g. url or urlHandler).
	 * @param LayeredContent_Handler_Abstract $handler The handler which is being set to take over the default handler.
	 */
	public function setHandler($handlerName, $handler)
	{
		if (strstr($handlerName, 'Hanlder') === false) {
			$handlerName .= 'Handler';
		}
		$this->$handlerName = $handler;
	}
	
	/**
	 * Sets or adds to the config objects already created.
	 *
	 * @param Zend_Config $config The config object being set.
	 * @param boolean $merge Whether to merge the configs. Note, $config must be writable.
	 */
	public function setConfig($config, $merge = false)
	{
		$this->config = ($merge && $this->config) ? $config->merge($this->config) : $config;
	}
	
	
	/**
	 * This will process the request, find the specified resource, and return
	 * the data. It will also handle 404 Page Not Found errors when there is no
	 * resource found for this request.
	 */
	public function run()
	{
		$action = $this->getAction();
		
		// first check if the cache exists, if it does, send it to the browser
		if ($action == 'view') {
			if ($this->hasCache()) {
				if (!$this->isCachedByBrowser()) {
					echo $this->getCache();
				}
				return;
			}
			
			$data = $this->getData();
			// TODO finish :)
		}
		
		$resource = $this->dataHandler->getResource($this->urlHandler->getFullPath());
		
		
		ob_start();
		$resource = $this->dataHandler->getPage($this->urlHandler->getFullPath());
		
		foreach ($this->dataHandler->times as $key => $time) {
			header("x-$key: $time");
		}
		
		ob_end_flush();
		
		if (!$this->dataHandler->success) {
			$this->send404();
		}
		
		// are we pointing to a valid resource, or a list? If 
		//$this->resourceHandler->
		
		//$resource = $this->dataHandler->getResource($this->urlHandler->getFullPath());
		//var_dump($resource);
		//$this->process();
	}
	
	/**
	 * Handles an existing web framework's 404 by allowing Layered Content to
	 * try and find the resource. Call this when the framework is not able to
	 * pass off a request to a controller/action and is going to send a 404 to
	 * the browser. Layered Content will look for a resource with this URL, and
	 * if it doesn't find one will allow the web framework to continue to its
	 * 404 error resource. Otherwise Layered Content will display the needed
	 * resource. The only difference between this method and run() is that run()
	 * takes care of 404s itself while this allows the framework to handle them.
	 */
	public function handle404()
	{
		
	}
	
	/**
	 * Use before init() if the current URL isn't the one that this resource
	 * should be stored under.
	 *
	 * @param string $url The format should be /my/url, mydomain.com/my/url, or
	 * http://mydomain.com/my/url
	 */
	public function setURL($url)
	{
		$this->urlHandler->setURL($url);
	}
	
	/**
	 * Returns the currently set URL Layered Content will be using. If none has
	 * been set then it will be the current URL in the browser. Or, if a partial
	 * URL is passed in this will return the full URL for that. 
	 * 
	 * @param string An optional URL which will be parsed and returned as a full
	 * URL. If none is passed in the default is to return the currently set URL.
	 * @return string The full URL (e.g. http://mydomain.com/my/url)
	 */
	public function getURL($url = null)
	{
		return $this->urlHandler->getURL($url);
	}
	
	/**
	 * The action this request wants to perform. Should be list, view, create,
	 * update, or delete.
	 */
	public function getAction()
	{
		return $this->urlHandler->action;
	}
	
	
	/**
	 * Initialize a request with Layered Content so that data for the current
	 * location may be stored and used. This method is to be used in conjunction
	 * with existing code and frameworks. Use the setURL method if the current
	 * URL isn't where you'd like to pull data from. initialize() should be
	 * called before getData(), getTemplate(), etc.
	 * 
	 * Only call this on resources that represent the data you are pulling. If
	 * you are simply getting the data from another resource, call setURL() and
	 * getData() without calling initialize().
	 */
//	public function initialize()
//	{
		
		// use this to determine what fields will be available for this resource
		
		// look for the right query strings to see if we should be returning
		//data to the browser instead of progromatically
		
		// allow for other options to be set for this resource
//	}
	
	
	/**
	 * Determines if there is an existing cache for this resource. Allows
	 * systems that don't utilize caching to tie into Layered Content's caching.
	 * By calling this method to check for a cache, it also initializes Layered
	 * Content to create a cache for this resource by capturing the ouput and
	 * saving the cache. Then the next request may use that cache.
	 * 
	 * @return boolean Whether or not the resource has a valid cache. If it
	 * does, get and use the cache with getCache()
	 */
	public function hasCache()
	{
		return $this->cacheHandler->hasCache();
	}
	
	/**
	 * If there is an existing cache for this resource, checks the date modified
	 * and the browser's headers that were sent to determine if the browser has 
	 * a current cache on the client side. If so, sends a 304 Not Modified
	 * header and no cache is needed to be sent. In addition to being faster
	 * this also saves bandwith. This method will also send the Last-Modified
	 * and ETag headers to the browser so that next time the browser will have
	 * it cached.
	 * 
	 * @return boolean Whether or not the resource has a valid cache. If it
	 * does, get and use the cache with getCache()
	 */
	public function isCachedByBrowser()
	{
		return $this->cacheHandler->isCachedByBrowser();
	}
	
	/**
	 * Returns the cache as a string. This may then be sent to the browser. You
	 * may optionally specify to send the cache directly to the browser and exit
	 * the script immediately with the $final parameter.
	 * 
	 * @param boolean $final This indicates that instead of returning the cache
	 * for you to send to the browser, let Layered Content send it to the
	 * browser and exit the running script. The default is false.
	 * @return void|string The string data for the cache if $final was not set
	 * to true.
	 */
	public function getCache($final = false)
	{
		$contents = $this->cacheHandler->getCache();
		if ($final)
			die($contents);
		
		return $contents;
	}
	
	/**
	 * Deletes any cache which may exist for this resource. If none exists it
	 * will simply return. Use this when you are using the caching feature in
	 * conjunction with your own data and queries to a database. If you are only
	 * using the data from Layered Content you don't need to worry about
	 * deleting the cache as Layered Content takes care of all that itself.
	 */
	public function deleteCache()
	{
		$this->cacheHandler->deleteCache();
	}
	
	/**
	 * Returns an associative array of the data this resource holds. This data
	 * is specified in init() and stored for this resource. It is also editable
	 * through Layered Content's APIs. This can be added to the data going into
	 * your templates.
	 *
	 * @return array An associative array of the data belonging to this
	 * resource.
	 */
	public function getData()
	{
		return $data;
	}
	
	/**
	 * If using Layered Content's templating system you can add your own data
	 * for use and display in the template. Note that this data cannot be
	 * modified or saved from Layered Content's API.
	 *
	 * @param array $data Associative array of data you'd like to be added to
	 * the template.
	 */
	public function addData($data)
	{
		
	}
	
	/**
	 * Allows you to use Layered Content's templating system and caching while
	 * still being able to pass in your own data. If you want Layered Content to
	 * be able to change the template, pass in null for the first parameter.
	 * 
	 * Only custom data needs to be passed into the $data parameter. This method
	 * will automatically add the content from the getData() method. 
	 * 
	 * @param string $template The template name to be used
	 * @param array $data The data to be passed into the template
	 * @return string The ouput from the template
	 */
	public function runTemplate($template = null, $data = null)
	{
		return $template;
	}
	
	/**
	 * Performs a search on the content/data in the system. Specifying a URL
	 * will limit the search to the specified resource and anything under it
	 * (e.g. /blog or /store/products).
	 *
	 * @param string $query The query typed in by a user
	 * @param string $url The base URL from which to search. A value of null
	 * searches the entire site. The default is null.
	 * @return Returns an array of hit objects which contain the URL, the score
	 * in terms of how close a match, and is ordered by the highest score,
	 * descending.
	 */
	public function search($query, $url = null)
	{
		
	}
	
	
	/**
	 * Saves data for current resource (or the one that has been set with
	 * setURL). 
	 * 
	 * @param array $data An associative array with the data to be saved to the
	 * resource. This is the data that is returned with getData() which is used
	 * in the resource.
	 */
	public function saveData($data)
	{
		
	}
	
	/**
	 * Saves new information for the current resource. If the URL is changing or
	 * if you are using the templates and the template changes, use this method
	 * to save those. Note, if you are saving a new URL you should redirect to
	 * the new URL after this save.
	 *
	 * @param string $url The new URL of the resource being saved.
	 */
	public function saveResource($url)
	{
		
	}
	
	/**
	 * Issue a redirect code, either a permanent (the default) or a temporary
	 * redirect. 
	 *
	 * @param string $url The URL to redirect to.
	 * @param boolean $permanent Whether to use a permanent redirect or not. The
	 * default is true.
	 */
	public function redirect($url, $permanent = true)
	{
		if ($permanent)	{
			header('HTTP/1.1 301 Moved Permanently');
		} else {
			header('HTTP/1.1 307 Temporary Redirect');
		}
		
		// make sure this is an absolute path, not relative
		$url = $this->getURL($url);
		
		header("Location: $url");
	}
	
	/**
	 * Sends a 404 status to the server along with the default 404 template.
	 * Also outputs the reason why a 404 was found if we're in development mode.
	 * 
	 * @param string $message The error message to be sent to the resource when
	 * development mode is on.
	 */
	public function send404($message = null) {
		header("HTTP/1.1 404 Page Not Found");
		// TODO use the templating system to handle this corretly
		//readfile(TEMPLATES . "404.html");
		echo "<h1>404 Page Not Found</h1>";
		
		if ($message && $this->config->debug) {
			echo "<font style='font-weight:bold;color:red'>$message</font>";
		}
		exit;
	}
	
	/**
	 * Sets up and initializes a layered content handler so that they all have
	 * references to each other. Will also be called on $this as its own handler
	 *
	 * @param LayeredContent_Handler $handler
	 */
	protected function initHandler($handler)
	{
		$handler->config = $this->config;
		$handler->urlHandler = $this->urlHandler;
		$handler->resourceHandler = $this->resourceHandler;
		$handler->cacheHandler = $this->cacheHandler;
		$handler->dataHandler = $this->dataHandler;
		$handler->init();
	}
	
	
	protected function loadConfig()
	{
		// Load Configuration
		require_once 'Zend/Config/Ini.php';
		if (!$this->config) {
			$this->config = new Zend_Config(array(), true);
		}
		$this->config->merge(new Zend_Config_Ini($this->root . '/config/main.ini'));
		$this->config->resources = new Zend_Config_Ini($this->root . '/config/resources.ini');
	}
	
	
	protected function process()
	{
		// if the cache doesn't exist, load the system up
		require_once 'Zend/Db.php';
		require_once 'Zend/Db/Table/Abstract.php';
		require_once 'Zend/Controller/Front.php';
		
		$config = Zend_Registry::get('config');
		
		// Register the database connection (won't connect until we use it)
		$db = Zend_Db::factory($config->database->adapter, $config->database);
		Zend_Registry::set('db', $db);
		Zend_Db_Table_Abstract::setDefaultAdapter($db);
		
		
		// Set up the front controller and dispatch
		$front = Zend_Controller_Front::getInstance();
		$front->getRouter()->addConfig($config, 'routes');
		$front->throwExceptions($config->debug);
		$front->setControllerDirectory($this->appRoot . '/controllers');
		$front->returnResponse(true);
		return $front->dispatch();
	}
	
}